package org.moonlight.jvm.classpath;

import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
import org.moonlight.jvm.classpath.entry.WildcardEntry;
import org.moonlight.jvm.cmd.Cmd;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * Oracle的Java虚拟机实现根据类路径（classpath）来搜索类。按照搜索的先后顺序，类路径可以分为以下三种:
 *   1. 启动 类路径（bootstrap classpath）：默认对应·jre\lib目录，Java标准库（大部分在rt.jar里）位于该路径。
 *   2. 扩展 类路径（extension classpath）：默认对应·jre\lib\ext目录，使用Java扩展机制的类位于这个路径。
 *   3. 用户 类路径（user classpath）：我们自己实现的类，以及第三方类库则位于用户类路径，位于当前目录，也可以通过-cp和-classpath
 * 当 JVM 需要一个类时，就会按照上述顺序在类路径中查找，如果在启动类路径中找不到，就会去扩展类路径寻找，最后才去用户类路径寻找：
 *
 * @author Moonlight
 * @createTime 2023/8/14 16:06
 **/
@EqualsAndHashCode
public class Classpath implements Entry {

    /** 启动 类路径 **/
    private Entry bootstrapClasspath;
    /** 扩展 类路径 **/
    private Entry extensionClasspath;
    /** 用户 类路径 **/
    private Entry userClasspath;

    public Classpath(String jrePath, String cpPath) {
        // 启动 类路径 和 扩展 类路径 由 jre 提供
        parseBootAndExtClasspath(jrePath);
        // 解析 用户 类路径
        parseUserClasspath(cpPath);
    }

    /**
     * 根据类名获取一个类的字节码
     * 根据双亲委派机制，按顺序读取，并且在前两个读取不到时不会报错
     *
     * @param className 待读取类的类名 - 这个参数不包含 .class 后缀
     * @return byte[] 读取到的类文件的内容
     * @createTime 16:46 2023/8/14
     * @author moonlight
     **/
    public byte[] readClass(String className) throws IOException {
        className += ".class";

        try {
            return this.bootstrapClasspath.readClass(className);
        } catch (IOException ignore) {}

        try {
            return this.extensionClasspath.readClass(className);
        } catch (IOException ignore) {}

        return this.userClasspath.readClass(className);
    }

    /** 
     * 解析并构建 启动 类路径  和  扩展 类路径. 具体 寻找 jre 路径顺序 请见 {@link Classpath#getJreDir(String)}
     * @param jrePath jre 配置路径 - 如果传入这个参数，那么它应该是从命令行参数传入通过 {@link Cmd#getJre()} 获取的。
     * @createTime 16:33 2023/8/14
     * @author moonlight
     **/
    private void parseBootAndExtClasspath(String jrePath) {
        String jreDir = getJreDir(jrePath);

        // 启动类在 jre/lib/*
        String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
        this.bootstrapClasspath = new WildcardEntry(jreLibPath);
        // 扩展类在 jre/lib/ext/*
        String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
        this.extensionClasspath = new WildcardEntry(jreExtPath);
    }

    /**
     * 寻找 jre 路径顺序
     *   1. 用户通过命令行参数指定的 jre 路径
     *   2. 在当前文件夹下寻找 jre 文件夹
     *   3. 通过系统环境变量 JAVA_HOME 查找 jre 文件夹
     *   4. 以上都找不到就直接 throw RuntimeException
     *
     * @param jrePath jre 配置路径 - 如果传入这个参数，那么它应该是从命令行参数传入通过 {@link Cmd#getJre()} 获取的
     * @return String 实际的 jre 路径
     * @createTime 16:16 2023/8/14 
     * @author moonlight
     **/
    private String getJreDir(String jrePath) {
        if (jrePath != null && Files.exists(Paths.get(jrePath))) {
            return jrePath;
        }
        if (Files.exists(Paths.get("./jre"))) {
            return "./jre";
        }
        String jh = System.getenv("JAVA_HOME");
        if (jh != null) {
            return Paths.get(jh, "jre").toString();
        }
        throw new RuntimeException("找不到 jre 路径!");
    }

    /**
     * 解析并构建 用户 类路径
     * @param cpPath 用户类路径 - 如果用户没有提供-classpath/-cp选项，则使用当前目录作为用户类路径。
     *                          如果提供了，那么它应该是从命令行参数传入通过 {@link Cmd#getClasspath()} 获取。
     * @createTime 16:31 2023/8/14
     * @author moonlight
     **/
    private void parseUserClasspath(String cpPath) {
        if (StringUtils.isBlank(cpPath)) {
            // 如果用户没有提供-classpath/-cp选项，则使用当前目录作为用户类路径
            cpPath = ".";
        }
        this.userClasspath = Entry.create(cpPath);
    }

    @Override
    public String toString() {
        return this.userClasspath.toString();
    }
}
